作业 策略权限功能:RolePolicy 创建服务
策略权限(Policy-Based Access Control)在 RBAC 基础上提供更细粒度的控制。RBAC 只能控制"用户能否访问某个接口",而策略权限可以控制"用户能否对某个资源执行某个操作"。本作业实现完整的 RolePolicy CRUD 服务和策略检查流程。
策略权限与 RBAC 的对比
| 维度 | RBAC | Policy-Based |
|---|---|---|
| 控制粒度 | 接口级别 | 资源/字段/条件级别 |
| 典型场景 | 用户能否访问 GET /users | 用户能否更新 Article 的 published 字段 |
| 条件支持 | 无 | 支持(如 authorId 匹配) |
| 数据模型 | Role → Permission | Role → Policy (含 conditions) |
Policy 数据库表设计
model Policy {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// 类型: 0=JSON条件, 1=MongoDB查询, 2=函数条件
type Int
/// 判断: 'can' | 'cannot'
effect String
/// 操作: create, read, update, delete, manage
action String
/// 资源: 类名如 'Article', 'User'
subject String
/// 字段控制 (可选)
fields Json?
/// 条件 (可选,结构因 type 而异)
conditions Json?
/// 函数参数 (仅 type=2)
args Json?
// 关联
rolePolicies RolePolicy[]
permissionPolicies PermissionPolicy[]
@@map("policies")
}
model RolePolicy {
roleId Int @map("role_id")
policyId Int @map("policy_id")
role Role @relation(fields: [roleId], references: [id])
policy Policy @relation(fields: [policyId], references: [id])
@@id([roleId, policyId])
@@map("role_policies")
}
model PermissionPolicy {
permissionId Int @map("permission_id")
policyId Int @map("policy_id")
permission Permission @relation(fields: [permissionId], references: [id])
policy Policy @relation(fields: [policyId], references: [id])
@@id([permissionId, policyId])
@@map("permission_policies")
}
prisma
DTO 定义
// src/policy/dto/create-policy.dto.ts
import { IsInt, IsString, IsOptional, IsEnum, IsArray } from 'class-validator';
import { Type } from 'class-transformer';
export class CreatePolicyDto {
@IsInt()
@Type(() => Number)
type: number;
@IsString()
@IsEnum(['can', 'cannot'])
effect: 'can' | 'cannot';
@IsString()
action: string;
@IsString()
subject: string;
@IsOptional()
fields?: Record<string, any>;
@IsOptional()
conditions?: Record<string, any>;
@IsOptional()
args?: Record<string, any>;
}
export class UpdatePolicyDto {
@IsOptional()
@IsInt()
@Type(() => Number)
type?: number;
@IsOptional()
@IsString()
effect?: string;
@IsOptional()
@IsString()
action?: string;
@IsOptional()
@IsString()
subject?: string;
@IsOptional()
fields?: Record<string, any>;
@IsOptional()
conditions?: Record<string, any>;
@IsOptional()
args?: Record<string, any>;
}
typescript
Policy Service
// src/policy/policy.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePolicyDto, UpdatePolicyDto } from './dto';
@Injectable()
export class PolicyService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreatePolicyDto) {
return this.prisma.policy.create({
data: dto,
});
}
async findAll(page: number = 1, limit: number = 10) {
const [items, total] = await Promise.all([
this.prisma.policy.findMany({
skip: (page - 1) * limit,
take: limit,
include: { rolePolicies: true },
}),
this.prisma.policy.count(),
]);
return { items, total, page, limit };
}
async findOne(id: number) {
return this.prisma.policy.findUnique({
where: { id },
include: { rolePolicies: true, permissionPolicies: true },
});
}
async update(id: number, dto: UpdatePolicyDto) {
return this.prisma.policy.update({
where: { id },
data: dto,
});
}
async remove(id: number) {
return this.prisma.policy.delete({ where: { id } });
}
/**
* 检查角色是否拥有特定策略
*/
async checkPolicy(roleIds: number[], subject: string, action: string) {
const policies = await this.prisma.policy.findMany({
where: {
rolePolicies: {
some: { roleId: { in: roleIds } },
},
subject,
action,
},
});
return policies.length > 0;
}
/**
* 获取角色关联的所有 Policy
*/
async findByRole(roleId: number) {
return this.prisma.policy.findMany({
where: {
rolePolicies: {
some: { roleId },
},
},
});
}
/**
* 为角色添加 Policy 关联
*/
async addPolicyToRole(roleId: number, policyId: number) {
return this.prisma.rolePolicy.create({
data: { roleId, policyId },
});
}
/**
* 移除角色的 Policy 关联
*/
async removePolicyFromRole(roleId: number, policyId: number) {
return this.prisma.rolePolicy.delete({
where: { roleId_policyId: { roleId, policyId } },
});
}
}
typescript
Policy Controller
// src/policy/policy.controller.ts
import {
Controller, Get, Post, Patch, Delete,
Body, Param, Query, ParseIntPipe,
} from '@nestjs/common';
import { PolicyService } from './policy.service';
import { CreatePolicyDto, UpdatePolicyDto } from './dto';
@Controller('policies')
export class PolicyController {
constructor(private readonly policyService: PolicyService) {}
@Post()
create(@Body() dto: CreatePolicyDto) {
return this.policyService.create(dto);
}
@Get()
findAll(
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.policyService.findAll(+page || 1, +limit || 10);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.policyService.findOne(id);
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdatePolicyDto,
) {
return this.policyService.update(id, dto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.policyService.remove(id);
}
@Get('role/:roleId')
findByRole(@Param('roleId', ParseIntPipe) roleId: number) {
return this.policyService.findByRole(roleId);
}
}
typescript
策略权限的运行流程
用户请求 (如 PUT /articles/123)
│
├─ 1. AuthGuard 验证 JWT → 获取 userId
│
├─ 2. PolicyGuard 执行
│ │
│ ├─ 路径 A: 读取接口关联的 Policy (Permission → PermissionPolicy → Policy)
│ │ → requiredPolicies: [{ action: 'update', subject: 'Article', fields: [...] }]
│ │
│ ├─ 路径 B: 读取用户角色的 Policy (User → Role → RolePolicy → Policy)
│ │ → userPolicies: [{ action: 'update', subject: 'Article', conditions: {...} }]
│ │
│ ├─ 3. CaslAbilityService.buildAbility(userPolicies, currentUser)
│ │ → Ability[] (用户权限实例)
│ │
│ └─ 4. 遍历 requiredPolicies,用 ability.can() 判断
│ → 所有策略通过 → 放行
│ → 任一策略失败 → 403 Forbidden
│
└─ 5. Controller 处理请求
text
Policy 数据示例
基础 JSON 条件 (type=0)
{
"type": 0,
"effect": "can",
"action": "update",
"subject": "Article",
"fields": { "type": "array", "data": ["title", "description"] },
"conditions": { "type": "object", "data": { "authorId": 1 } }
}
json
MongoDB 查询条件 (type=1)
{
"type": 1,
"effect": "can",
"action": "read",
"subject": "Article",
"conditions": {
"type": "object",
"data": { "$or": [{ "authorId": 1 }, { "private": false }] }
}
}
json
函数条件 (type=2)
{
"type": 2,
"effect": "can",
"action": "update",
"subject": "Article",
"conditions": { "type": "string", "data": "authorId === user.id" },
"args": { "type": "array", "data": ["user"] }
}
json
策略权限相比 RBAC 的核心优势在于:不仅能控制"能否访问接口",还能精确控制"能操作哪些字段"、"在什么条件下才能操作",实现从接口级到数据级的完整权限体系。
↑